﻿using System.Data;
using System;
using System.Collections.Generic;
using DbView;
namespace SqlServerDriver
{
	/* 
		interface for classes that wish to synchronise from 
		a source list to a target list and map changes
		The lists must be sorted on the value used for the Comparison. The 
		value may either be the entire buffer or a defined key section depending on 
		the struture being synched
		Each list should maintain a separate the act of addressing the 
		data to read and actually reading it into a buffer for comparison
		The interface separates moving the pointer to data actaully dereferencing it
	*/
	interface ISyncList
	{
		// reset the pointer to the first item in the list
		void MoveFirst();
		// move the pointer to the next item 
		void MoveNext();
		// return t/f depending on whether or not the pointer is moved past the last item
		bool AtEnd();
		// transfer data from the current pointer location to the comparison location
		void Read();
		// compare contents of the key section of the comparison item with those of another list 
		// return -1 if the other key is greater, 1 if the other key is less or 0 if the keys are equal
		int Compare(ISyncList other);
	}
	/*
		Class to preform synchronisation on two objects implementing the ISynchList interface.
		Normally the objects are expected to belong to the same class, but you never know!
		
		The class implements a single static synchronisation method.
		
		Three delegates are defined to manage the implamentation of the operation that the 
		Synch method has determined is required. The assume that the target object is being synch'd
		to the source object.
	*/	
	class Synchronizer
	{
		// 
		public delegate void Delete(ISyncList targetItem);
		public delegate void Add(ISyncList sourceItem, ISyncList target);
		public delegate void Update(ISyncList sourceItem, ISyncList target);
		
		public static void Synch(ISyncList srcList, ISyncList tgtList, Delete onDelete, Add onAdd, Update onUpdate)
		{
			// start by setting the pointer to the start of each list.
			// Note: this should cause AtEnd() to return true if a list has no items
			srcList.MoveFirst();
			tgtList.MoveFirst();
			int synchStatus = 0; // set initial status to equal to force two reads on non-empty lists
			// loop until both lists are exhausted.
			while (!(srcList.AtEnd() && tgtList.AtEnd()))
			{
				if (srcList.AtEnd())
				{
					// source empty target not empty. 
					tgtList.Read();
					// Delete from target as it is not in source
					onDelete(tgtList);
					// on to the next (will delete until the end)
					tgtList.MoveNext();
				}
				else if (tgtList.AtEnd())
				{
					// target empty, source has items
					srcList.Read();
					// add to target as it is not already there
					onAdd(srcList, tgtList);
					// on to the next (will add until the end of the source list).
					srcList.MoveNext();
				}
				else
				{
					// Ah. neither list is exhausted. 
					// Use the result of the previous comparison to determine what to 
					// load into the comparison buffer
					switch (synchStatus)
					{
						case -1: // src behind target
							srcList.Read();
							break;
						case 1: // src ahead of target
							tgtList.Read();
							break;
						case 0: // src is level with target
							srcList.Read();
							tgtList.Read();
							break;
					}
					// following the buffer refresh compare again ...
					synchStatus = srcList.Compare(tgtList);
					// and act on the result of the comparison
					switch (synchStatus)
					{
						case -1: // src behind target means src needs adding to target
							onAdd(srcList, tgtList); 
							srcList.MoveNext(); // move src, but not tgt pointer
							break;
						case 1: // src ahead of target means that something in target needs deleting
							onDelete(tgtList);
							tgtList.MoveNext(); // move tgt, but not source pointer
							break;
						case 0: // key match 
							onUpdate(srcList, tgtList); // update target from source
							srcList.MoveNext();     // move both pointers on
							tgtList.MoveNext();
							break;
					}
				}
			}
		}
	}
	
	/*
		Class to implement the sync interface for the quick diff routine.
		Unfortunately the quick diff algorithm was written for a different purpose
		and while the comparison logic is still suitable the order in which the differences
		are reported needs some adjustment. Hence rather more state in this class than 
		originaly anticipated.
		Indeed the identification of items in one place not the other is not handled by 
		the algorithm above, but done in advance. This is because in the case of tables 
		and fields I wanted to distinguish between tables missing and fields missing.
		The sync algorithm would cause all fields for missing table to be shown, creating
		redundant rows.
	*/
	internal class DataTableSync : ISyncList
	{
		internal DataTableSync(DataTable dt, int[] keyCols)
		{
			this.theData = dt;
			this.keyColumns = keyCols;
			this.last = dt.Rows.Count;
			this.MoveFirst();
		}
		public bool AtEnd()
		{
			return (this.pointer == -9);
		}

		public void MoveFirst()
		{
			this.pointer = (this.last == 0) ? -9 : 0;
			// changes has same schema.
			this.Changes = this.theData.Clone();
		}

		public void MoveNext()
		{
			if (this.pointer == -9) return;
			++this.pointer;
			if (this.pointer >= this.last)
				this.pointer = -9;
		}

		public void Read()
		{
			if (this.pointer == -9) return;
			this.currentRow = this.theData.Rows[this.pointer];
		}

		public int Compare(ISyncList target)
		{
			DataTableSync tgt = target as DataTableSync;
			if (this.currentRow == null) return -1;
			if (tgt.currentRow == null) return 1;
			// compare key fields
			int comp = 0;
			foreach (int idx in this.keyColumns)
			{
				comp = this.CompareOneField(tgt, idx);
				if (comp != 0) return comp;
			}
			return 0;
		}

		private int CompareOneField(DataTableSync tgt, int idx)
		{
			bool tgtIsNull = (tgt.currentRow[idx].GetType() == typeof(System.DBNull));
			bool thisIsNull = (this.currentRow[idx].GetType() == typeof(System.DBNull));
			if (tgtIsNull)
				return (thisIsNull) ? 0 : 1;
			if (thisIsNull) return -1;
			IComparable comp = this.currentRow[idx] as IComparable;
			// source code test
			// done here so original source code is shown in the report
			if (this.currentRow[idx].GetType() == typeof(System.String))
			{
				String data = this.currentRow[idx].ToString();
				// check whether the string contains any signature of a piece of code
				// ie line-breaks, tabs or multiple spaces.
				if (data.IndexOf("\n") != -1 || data.IndexOf("\t") != -1 || data.IndexOf("  ") != -1)
				{
					comp = this.MakeCodeCompString(data) as IComparable;
					return comp.CompareTo(this.MakeCodeCompString(tgt.currentRow[idx].ToString()));
				}
			}
			// normally 
			return comp.CompareTo(tgt.currentRow[idx]);
		}
		
		// eliminates most of the trivial differences in source code.
		String MakeCodeCompString(String code)
		{
			String tmp = code.Trim().           // remove normally unimportant leading and trailing
							Replace("\t", " "). // eliminate tabs
							Replace("\r", "");  // deal with \r\n same as \n
			while (tmp.IndexOf("  ") != -1)		// reduce multi-spaces to a single space
				tmp = tmp.Replace("  ", " ");
			return tmp.ToLower();				// deal with case.
		}
		
		enum RowType { add, del, same, change };

		internal void Add(ISyncList src, ISyncList tgt)
		{
			DataTableSync srcTable = src as DataTableSync;
			DataTableSync tgtTable = tgt as DataTableSync;
			// now only if a field 
			String key = this.MakeKey(srcTable.currentRow);
			if (!this.otherKeys.ContainsKey(key))
			{
				// add from src to this
				srcTable.currentRow[0] = "+";
				this.Changes.ImportRow(srcTable.currentRow);
				// make this green.
				this.RegisterRow(this.Changes.Rows.Count - 1, RowType.add);
			}
		}

		internal void Update(ISyncList src, ISyncList tgt)
		{
			DataTableSync srcTable = src as DataTableSync;
			DataTableSync tgtTable = tgt as DataTableSync;
			int fieldCount = tgtTable.currentRow.ItemArray.GetLength(0);
			// update this with src
			int comp = 0;
			for (int idx = 0; idx < fieldCount; ++idx)
			{
				comp = this.CompareOneField(srcTable, idx);
				if (comp != 0) break;
			}

			// do some diffing here
			if (comp != 0)
			{
				// this is the source ie. the ideal
				srcTable.currentRow[0] = ">";
				this.Changes.ImportRow(srcTable.currentRow);
				// make this blue.
				this.RegisterRow(this.Changes.Rows.Count - 1, RowType.change);
				// this is the original version. No colouring
				tgtTable.currentRow[0] = "<";
				this.Changes.ImportRow(tgtTable.currentRow);
			}
		}

		internal void Del(ISyncList tgt)
		{
			DataTableSync tgtTable = tgt as DataTableSync;
			String key = this.MakeKey(tgtTable.currentRow);
			if (!this.keys.ContainsKey(key))
			{
				tgtTable.currentRow[0] = "-";
				this.Changes.ImportRow(tgtTable.currentRow);
				// make this red
				this.RegisterRow(this.Changes.Rows.Count - 1, RowType.del);
			}
		}

		public void MapExclusiveRows(DataTableSync other)
		{
			// make list of all the keys in this object
			foreach (DataRow dr in this.theData.Rows)
			{
				String key = this.MakeKey(dr);
				if (!this.keys.ContainsKey(key)) this.keys.Add(key, 0);
			}
			// compare each key in the the object once and take appropriate action
			String prevKey = "";
			foreach (DataRow dr in other.theData.Rows)
			{
				String key = this.MakeKey(dr);
				if (key != prevKey)
				{
					// in this then in both so eliminate from this
					if (this.keys.ContainsKey(key))
						this.keys.Remove(key);
					// not in this so add
					else
						this.otherKeys.Add(key, 0);
					prevKey = key;
				}
			}
		}

		public void AddNew(DataTableSync other)
		{
			String prevKey = "";
			foreach (DataRow dr in other.theData.Rows)
			{
				String key = this.MakeKey(dr);
				if (key != prevKey)
				{
					// in this then in both so eliminate from this
					if (this.otherKeys.ContainsKey(key))
					{
						dr[0] = "+";
						this.Changes.ImportRow(dr);
						this.ClearRow(this.Changes.Rows[this.Changes.Rows.Count - 1]);
						this.RegisterRow(this.Changes.Rows.Count - 1, RowType.add);
					}
					prevKey = key;
				}
			}
		}

		public void DeleteOld()
		{
			String prevKey = "";
			foreach (DataRow dr in this.theData.Rows)
			{
				String key = this.MakeKey(dr);
				if (key != prevKey)
				{
					// in this then in both so eliminate from this
					if (this.keys.ContainsKey(key))
					{
						dr[0] = "-";
						this.Changes.ImportRow(dr);
						this.ClearRow(this.Changes.Rows[this.Changes.Rows.Count - 1]);
						this.RegisterRow(this.Changes.Rows.Count - 1, RowType.del);
					}
					prevKey = key;
				}
			}
		}

		private string MakeKey(DataRow dr)
		{
			// 1st data itam (dr[0] is status)
 			return dr[1].ToString();
		}

		// for a whole missing table clear the field info from the 
		// cells to leave just the table name.
		void ClearRow(DataRow dr)
		{
			for (int idx = 2; idx < dr.ItemArray.GetLength(0); ++idx)
				dr[idx] = System.DBNull.Value;
		}

		private void RegisterRow(int rowIndex, RowType rowtype)
		{
			switch (rowtype)
			{
				case RowType.add: this.greenRows.Add(rowIndex); break;
				case RowType.del: this.redRows.Add(rowIndex); break;
				case RowType.change: this.blueRows.Add(rowIndex); break;
			}
		}

		public void MarkRows()
		{
			DataTableExtraInfoManager colourer = new DataTableExtraInfoManager(this.Changes);
			
			if (this.redRows.Count > 0)
				colourer.SetRowColour("Red", this.redRows.ToArray());
			if (this.greenRows.Count > 0)
				colourer.SetRowColour("Green", this.greenRows.ToArray());
			if (this.blueRows.Count > 0)
				colourer.SetRowColour("Blue", this.blueRows.ToArray());
		}

		DataTable theData;
		int[] keyColumns;
		int last = 0;
		DataRow currentRow;  // used in sync
		int pointer;			// used in sync

		internal DataTable Changes { get; set; }  // holds the final report
		
		List<int> redRows = new List<int>();		// pointers to objects not in source
		List<int> greenRows = new List<int>();		// pointers to objects not in target
		List<int> blueRows = new List<int>();		// pointers to objects in source with different attributes to target
		// maps of keys missing from opne side or the other.
		Dictionary<String, int> keys = new Dictionary<string,int>();
		Dictionary<String, int> otherKeys = new Dictionary<string,int>();
	}

	internal class QuickDiffEngine
	{
		internal QuickDiffEngine(DbViewDriver.IConnectionConfig connectionConfig)
		{
			this.connectionConfig = connectionConfig;
		}
		
		public static String[] ObjectsTypes
		{
			get 
			{
				// return new String [] {"Tables","Views"};
				return new SyncObjectFactory().SyncNames;
			}
		}
		
		internal void Configure(String db1, String db2, String objectType)
		{
			this.Db1 = db1;
			this.Db2 = db2;
			this.objectType = objectType;
		}
		
		internal bool PreExecuteCheck(out String preExecuteCheckMessage)
		{
			preExecuteCheckMessage = "OK";
			// check for m
			if (this.Db1 == null || this.Db2 == null)
			{
				preExecuteCheckMessage = "Engine is not properly configured with a source and target database";
				return false;
			}
			String objectName = "";
			try 
			{
				SyncObject so = new SyncObjectFactory()[this.objectType];
				if (so.Name.ToUpper().IndexOf("TABLE") == 0) return true; // any connection can compare tables
				objectName = so.Name;
			}
			catch (Exception)
			{
				preExecuteCheckMessage = "Engine is not properly configured with a proper database object type.";
				return false;
			}

			String connSource = "";
			String connTarget = "";
			List<DbView.ConnectionInfo> connections = this.connectionConfig.GetConnections();
			foreach (DbView.ConnectionInfo ci in connections)
			{
				if (ci.Name == this.Db1) connSource = SqlServerDataSource.MakeConnectionString(ci); // ci.connectionString;
				if (ci.Name == this.Db2) connTarget = SqlServerDataSource.MakeConnectionString(ci);
			}
			if (connSource.Length == 0 || connTarget.Length == 0)
			{
				preExecuteCheckMessage = "Source or target database configuration could not be resolved";
				return false;
			}
			String sql = "if exists(select 1 from fn_my_permissions(NULL, 'DATABASE') where permission_name='VIEW DEFINITION') select 1 else select 0";
			String res = Helper.GenericScalarQuery(connSource, sql, "");
			String message = "Source database connection does not have VIEW DEFINITION permission and therefore cannot access "+objectName+" source code";
			if (res == "0")
			{
				preExecuteCheckMessage = message;
				return false;
			}
			res = Helper.GenericScalarQuery(connTarget, sql, "");
			message = message.Replace("Source", "Target");
			if (res == "0")
			{
				preExecuteCheckMessage = message;
				return false;
			}

			return true;
		}
		
		internal DataTable Execute(String tableTitle)
		{
			// Establish source and target connections
			String connSource = "";
			String connTarget = "";
			List<DbView.ConnectionInfo> connections = this.connectionConfig.GetConnections();
			foreach (DbView.ConnectionInfo ci in connections)
			{
				if (ci.Name == this.Db1) connSource = SqlServerDataSource.MakeConnectionString(ci); // ci.connectionString;
				if (ci.Name == this.Db2) connTarget = SqlServerDataSource.MakeConnectionString(ci);
			}
			// create the two datatables  for comparison
			SyncObject so = new SyncObjectFactory()[this.objectType];
			DataSet dset1;
			Helper.GenericMakeDset(connSource, so.Query, "DB1", out dset1);
			DataSet dset2;
			Helper.GenericMakeDset(connTarget, so.Query, "DB2", out dset2);
			DataTable dt1 = dset1.Tables[0];
			DataTable dt2 = dset2.Tables[0];
			if (dt1.Columns["text"] != null) // The DSet has a text column.
			{
				// checks for long scripts in multiple rows and joins them
				dt1 = this.NormaliseSource(dt1);
				dt2 = this.NormaliseSource(dt2);
			}
			
			// prepare the sync objects with the datatables
			DataTableSync s = new DataTableSync(dt1, so.MatchColumns);
			DataTableSync s2 = new DataTableSync(dt2, so.MatchColumns);
			// The Synchronizer.Synch was developed for a different purpose. 
			// Turns out the QuickDiff reports need to be in a different order to the 
			// standard one. The first try uses all these workarounds
			if (so.ObjectIdColumn != -1)
				s2.MapExclusiveRows(s); // Map rows with Pkeys only in one or other of the datatables
			// run the sync. This will output the rows with changes, but not added or removed rows
			Synchronizer.Synch(s, s2, s2.Del, s2.Add, s2.Update);
			s2.AddNew(s); // now add the new objects
			s2.DeleteOld(); // now add the removed objects
			s2.MarkRows(); // now add colouring.
			// Its all in the dset returned.
			DataTable dt = s2.Changes;
			DataTableExtraInfoManager ex = new DataTableExtraInfoManager(dt);
			ex.GridDescription = tableTitle;
			return dt;
		}
	
		/*
			 Yukky code. When stored procedure code is long it may overflow into more
			 than one record in syscomments. This makes the checking very vulnerable to 
			 trivial whitespace differences. The only way to proceed is to concatenate the source
			 into a single string and then apply global normalising of whitespace on the whole
			 script. The method below performs the concatenation.
			 (Note: assumes the text is sorted by object then sequence)
		*/
		private DataTable NormaliseSource(DataTable dt)
		{
			// here with a table of source 
			// col 0 = " " col 1 = "Name" col 2 = seq col 3 = "text"

			// find all overflow rows
			DataRow[] extras = dt.Select("colid > 1");
			// iterate extras adding to start of script and removing the row from the out tab;e
			foreach (DataRow dr in extras)
			{
				// create a search for the start of the script in the table
				// based on the name in the current overflow row.
				String seek = String.Format("Name = '{0}' and colid = 1", dr["Name"]);
				// only ever 1 row returned despite the array
				DataRow[] row1 = dt.Select(seek);
				// concatenate
				row1[0]["text"] = row1[0]["text"].ToString() + dr["text"].ToString();
				// remove the overflow row from the output table
				dt.Rows.Remove(dr);
			}
			return dt;
		}
		
		private DbViewDriver.IConnectionConfig connectionConfig;

		String Db1 {get; set; }
		String Db2 { get; set; }
		String objectType;
	}

	internal class SyncObject
	{
		internal String Name {get; set; }
		internal int[] MatchColumns {get; set; }
		internal int ObjectIdColumn{ get; set; }
		internal String Query { get; set; }
	}

	/*
		Hacky class for configuring parameters for Quick Diff on operations.
	*/
	internal class SyncObjectFactory
	{
		internal SyncObjectFactory()
		{
			SyncObject so = new SyncObject();
			so.Name = "Tables";
			// sql server 2000-friendly
			so.Query = @"
SELECT ' ' as [Status], o.name as [Table Name], c.name as [Column], 
       type_name(c.xtype) as [Type],c.length 
  FROM syscolumns c
  join sysobjects o on o.id = c.id
 WHERE o.xtype in ('U') 
order by o.name, c.name";
			// sql server 2005+
			so.Query = @"
select     ' ' as [Status],
           t.name as [Table Name],
           cols.name as [Field Name],
           type_name(cols.user_type_id) as [Type], 
           case when cols.max_length = -1 then 'MAX' 
                else convert(varchar(20), case when cols.system_type_id in (231,239) 
                                               then cols.max_length/2 
                                               else cols.max_length end)
                end as [Length], 
           case cols.is_nullable when 0 then 'No' else '' end as [Nulls?], 
           case when ind.is_primary_key = 1 then 'Yes' else '' end as [PKey?],
           case when cols.is_identity = 1 then '('+LTRIM(STR(ISNULL(IDENT_SEED(cols.object_id),1)))+','
                                                  +LTRIM(STR(ISNULL(IDENT_INCR(cols.object_id),1)))+')' 
                                          else '' end as [Auto?],
           case when ind.is_non_pk_index = 0 then 'Yes' else '' end as [Index?],
           case when fkey.parent_object_id is not null then 'Yes' else '' end as [FKey?],
           case when c.definition is not null then 'Yes' else '' end as [Def?],
           case cols.is_computed when 1 then 'Yes' else '' end as [Calc?],
           coalesce(c.definition, def.definition,'') as [Default/Computed]
  from sys.columns cols
  join sys.tables t on cols.object_id = t.object_id
  left join (select ic.object_id, ic.column_id, 
			       max(cast(is_primary_key as tinyint)) as is_primary_key, 
				   min(cast(is_primary_key as tinyint)) as is_non_pk_index
				 from sys.index_columns ic 
				 join sys.indexes ind on ind.object_id = ic.object_id and ind.index_id = ic.index_id 
				group by ic.object_id, ic.column_id) ind on ind.object_id = t.object_id and ind.column_id = cols.column_id
  left join (select distinct parent_object_id, parent_column_id 
               from sys.foreign_key_columns) fkey on fkey.parent_object_id = cols.object_id 
                                                 and fkey.parent_column_id = cols.column_id
  left join sys.default_constraints c on c.parent_object_id = t.object_id and c.parent_column_id = cols.column_id 
  left join sys.computed_columns def on def.object_id = t.object_id and def.column_id = cols.column_id
  where t.type = 'U'
 order by t.name, cols.name
";
			so.MatchColumns = new int[]{1,2};
			so.ObjectIdColumn = 1;
			this.syncObjects.Add(so);

			so = new SyncObject();
			so.Name = "Views";
			so.Query = this.GetDiffSourceCodeSql("V");
			so.MatchColumns = new int[]{1,2};
			so.ObjectIdColumn = 1;
			this.syncObjects.Add(so);

			so = new SyncObject();
			so.Name = "Procedures";
			so.Query = this.GetDiffSourceCodeSql("P");
			so.MatchColumns = new int[] { 1,2 };
			so.ObjectIdColumn = 1;
			this.syncObjects.Add(so);

			so = new SyncObject();
			so.Name = "Triggers";
			so.Query = this.GetDiffSourceCodeSql("T");
			so.MatchColumns = new int[] { 1, 2 };
			so.ObjectIdColumn = 1;
			this.syncObjects.Add(so);

			so = new SyncObject();
			so.Name = "Functions";
			so.Query = this.GetDiffSourceCodeSql("FN','TF");   // hacky use of the parameter. But it works (shrug).
			so.MatchColumns = new int[] { 1, 2 };
			so.ObjectIdColumn = 1;
			this.syncObjects.Add(so);

			so = new SyncObject();
			so.Name = "Foreign Keys";
			so.Query = @"
select ' ' as [Status], 
		convert(sysname,o2.name) as TABLE_WITH_FK,
        convert(sysname,c2.name) as FK_FIELD,
        convert(sysname,o1.name) as PK_TABLE,
        convert(sysname,c1.name) as PKEY,
        OBJECT_NAME(k.constraint_object_id) as FK_NAME,
        case f.is_disabled when 1 then 'Y' else '' end as DISABLED,
        case f.is_not_for_replication when 1 then 'Y' else 'N' end as REPLICATION_SAFE,
        case f.delete_referential_action when 0 then 'NO ACTION' WHEN 1 THEN 'CASCADE' WHEN 2 THEN 'SET_NULL' end as DELETE_RULE,
        case f.update_referential_action when 0 then 'NO ACTION' WHEN 1 THEN 'CASCADE' WHEN 2 THEN 'SET_NULL' end as UPDATE_RULE
 from   sys.foreign_keys f 
 join   sys.foreign_key_columns k on k.constraint_object_id = f.object_id
 join   sys.objects o1 on o1.object_id = f.referenced_object_id
 join   sys.objects o2 on o2.object_id = f.parent_object_id
 join   sys.columns c1 on c1.object_id = f.referenced_object_id and 
                          c1.column_id = k.referenced_column_id
 join   sys.columns c2 on c2.object_id = f.parent_object_id and 
                          c2.column_id = k.parent_column_id
	 
 order by TABLE_WITH_FK, PK_TABLE, FK_FIELD, PKEY
";
			so.MatchColumns = new int[] { 1, 2, 3, 4 };
			so.ObjectIdColumn = -1;
			this.syncObjects.Add(so);
		}
		
		String GetDiffSourceCodeSql(String objectChar)
		{
			String template = @"
SELECT ' ' as [Status], 
		case when s.name = 'dbo' then so.name else s.name+'.'+so.name end as [Name], 
       sc.colid, sc.text 
  FROM sys.objects so 
  JOIN sys.schemas s on s.schema_id = so.schema_id
  JOIN sys.syscomments sc ON so.object_id  = sc.id 
 WHERE type in ('{0}')
ORDER BY 2,3 
";
			return String.Format(template, objectChar);
		}

		internal SyncObject this[String name]
		{
			get
			{
				foreach (SyncObject so in this.syncObjects)
				{
					if (so.Name == name) return so;
				}
				return null;
			}
		}
		
		internal String [] SyncNames
		{
			get
			{
				List<String> names = new List<string>();
				foreach (SyncObject so in this.syncObjects)
				{
					names.Add(so.Name);
				}
				return names.ToArray();
			}
		}
		
		List<SyncObject> syncObjects = new List<SyncObject>();
	}
}
